В этом модуле мы будем использовать библиотеку ggplot2 и plotly для создания интерактивной карты расстановки игроков на поле.

Функция отвечающая за отрисовку называется plot_scene. Она принимает в качестве аргументов:

Для отрисовки создается поле с разделенными по 10 ярдов зонами, двумя зонами тачдауна для каждой команды и линией положения мяча. В NFL используется игровое поле размером 100 на 53 ярда. Для предотврацения выхода за границы поле в функции ограничено - вылетающие значения обрезаются.

plot_scene <- function(offense_df, defense_df, ball_x, ball_y, title) {
  if (ball_x > 109){
    ball_x = 109
  }
  if(ball_x < 11){
    ball_x = 11
  }
  if (ball_y > 52){
    ball_y = 52
  }
  if(ball_y < 1){
    ball_y = 1
  }
  offense_df['role'] = 'offense'
  defense_df['role'] = 'defense'
  n_df <- rbind(offense_df, defense_df)
  ball <- data.frame(x=0, y=0, density=0, type='', Atype=0, role='ball')
  n_df <- rbind(n_df, ball)
  n_df$role <- as.factor(n_df$role)
  plt <- ggplot(n_df) + 
    annotate("rect", xmin=0, xmax=10, ymin=0, ymax=53, fill='red', alpha=.2) + 
    annotate("rect", xmin=110, xmax=120, ymin=0, ymax=53, fill='red', alpha=.2) + 
    annotate("rect", xmin=10, xmax=110, ymin=0, ymax=53, fill='green', alpha=.2) + 
    xlim(0, 120) + ylim(0, 53) + 
    geom_vline(xintercept=seq(0, 120, 10)) + 
    geom_vline(xintercept=ball_x, colour='red') +
    geom_hline(yintercept=seq(0, 53, 53)) + 
    geom_point(mapping=aes(x+ball_x, y+ball_y, colour=role), size=3) + 
    geom_text(mapping=aes(x+ball_x, y+ball_y, label=as.factor(type)), size=2.5) +
    ggtitle(title) +
    xlab("X") +
    ylab("Y") +
    labs(colour="Team") +
    scale_color_manual(values=c("#32CD32", "#6495ED", "#F08080"))
  ggplotly(plt)
}

Загрузка данных

Для моделирования положения игроков на поле используются данные, полученные с использованием heatmaps и аналитики самых удачных расположений атак. Так как в данных присутствуют не все данные для игроков (а на поле их 11 от каждой команды) была написана функция генерации позиций. Для всех отсутствующих позиций берется самая удачная для данной категории (DL, DB, LB) и с использованием равномерного распределения создается предполагаемая позиция игрока.

За генерацию отвечает функция all_missing, принимающая на вход фрейм с удачными расположениями и необходимое количество. Возвращает готовый фрейм с игроками.

add_missing <- function(df, cur_max) {
  if(nrow(df) < cur_max){
      diff = cur_max - nrow(df)
      df <-df[order(df$density),]
      for (i in 1:diff) {
        dx = runif(1,0,2)
        dy = runif(1,-5,5)
        new <- list(x=df[i,]$x + dx, y=df[i,]$y + dy, density=df[i,]$density, type=df[i,]$type, Atype=df[i,]$Atype)
        df <- rbind(df, new)
      }
  } else {
    df <- df[1:cur_max,]
  }
  return(df)
}

Функция создания набора позиций по формации formation_make. На вход принимает тип команды - 0, если это команда защиты и 1, если команда атаки. По умолчанию задан type=0. Вторым аргументом выступает слово отвечающая за тип формации. Возможные значения:

formation_make <- function(type=0, formation) {
  # bestper <- read.csv("bestper.csv")
  # bestpossDef <- read.csv("bestPosDef.csv")
  # bestpossDef <- bestpossDef[bestpossDef$density != 0,]
  # bestpossA <- read.csv("bestpossAtck.csv")
  if(type == 0){
    current <- bestper[bestper$offenseFormation == formation,]
    all_pos <- bestpossDef[bestpossDef$Atype == formation,]
    dl_pos <- all_pos[all_pos$type == 'DL',]
    if(nrow(dl_pos) == 0){
      dl_pos <- bestpossDef[bestpossDef$Atype == 'ALL' & bestpossDef$type == 'DL',]
    }
    lb_pos <- all_pos[all_pos$type == 'LB',]
    if(nrow(lb_pos) == 0){
      lb_pos <- bestpossDef[bestpossDef$Atype == 'ALL' & bestpossDef$type == 'LB',]
    }
    db_pos <- all_pos[all_pos$type == 'DB',]
    if(nrow(db_pos) == 0){
      db_pos <- bestpossDef[bestpossDef$Atype == 'ALL' & bestpossDef$type == 'DB',]
    }
    
    dl_pos <- add_missing(dl_pos, current$DL_def)
    lb_pos <- add_missing(lb_pos, current$LB_def)
    db_pos <- add_missing(db_pos, current$DB_def)
    
    field_df <- rbind(dl_pos, lb_pos, db_pos)
  } else {
    field_df <- bestpossA[bestpossA$Atype == formation,]
    for (i in 1:nrow(field_df)){
      if (field_df[i,]$density == 0){
         field_df[i,] <- bestpossA[bestpossA$Atype == 'ALL' & bestpossA$type == field_df[i,]$type,]
      }
    }
    if(nrow(field_df) > 10){
      field_df <- field_df[1:10,]
    }
  }
  return(field_df)
}

Использование

Положение мяча генерируется с использованием функции равномерного распределния runif(). Для получения позиций игроков в цикле будем использовать созданную нами функцию formation_make. Посмотрим на позиции команд при различных формациях атаки. Хотя формация атаки держится в тайне при начале игры, защита все равно в большинстве случаев различается в соответствии с формацией.

l <- htmltools::tagList()
i = 1
bestper <- read.csv("bestper.csv")
bestpossDef <- read.csv("bestPosDef.csv")
bestpossDef <- bestpossDef[bestpossDef$density != 0,]
bestpossA <- read.csv("bestpossAtck.csv")
for (form in c("ALL", "EMPTY", "I_FORM", "JUMBO", "PISTOL", "SHOTGUN", "SINGLEBACK", "WILDCAT")) {
  def_df <- formation_make(type=0, formation = form)
  off_df <- formation_make(type=1, formation = form)
  l[[i]] <- as.widget(plot_scene(off_df, def_df, runif(1,20,80), runif(1, 20, 33), paste("Моделирование игры NFL -", form)))
  i = i+1
}
l

Использование таких расположений позволяет по типу атаки сформировать положение команд на поле.

По итогам кластеризации были выявлены наиболее рискованные, а так же наименее рискованные ситауации на поле. Это значения относящиеся к 3 и 5 кластерам

plays_clustering <- read.csv('plays_clustering.csv')
plays_clustering <- plays_clustering[plays_clustering$epa_yardstogo == 3 | plays_clustering$epa_yardstogo == 5,]

fig <- ggplot(data = plays_clustering) + 
  geom_point(aes(yardsToGo, epa, colour=factor(epa_yardstogo))) +
  geom_hline(yintercept = 0) +
  labs(colour="Cluster") +
  scale_color_manual(values=c("#df0000", "#40ad00"))

ggplotly(fig)

Построим наилучшие позиции полученные с помощью heatmaps. Для этого в подготовленную функцию генерации позиций загружаем датасет possDefCust3 с лучшими позициями защиты под 3 кластер. Для команды атаки используем уже готовый датасет bestpossAtck.

bestpossDef <- read.csv("possDefCust3.csv")
bestpossA <- read.csv("bestpossAtck.csv")
def_df <- formation_make(type=0, formation = "ALL")
off_df <- formation_make(type=1, formation = "ALL")
plot_scene(off_df, def_df, runif(1,20,80), runif(1, 20, 33), "Наименее рискованная ситуация")

Для наименее рискованной ситуации, когда команде атаки надо пройти достаточно много ярдов для текщего дауна и для этого момента низкий EPA, загрузим датасет possDefCust5.

bestpossDef <- read.csv("possDefCust5.csv")
bestpossA <- read.csv("bestpossAtck.csv")
def_df <- formation_make(type=0, formation = "ALL")
off_df <- formation_make(type=1, formation = "ALL")
plot_scene(off_df, def_df, runif(1,20,80), runif(1, 20, 33), "Наименее рискованная ситуация")

Как и предполагалось, если осталось пройти немного ярдов, то защита стремится выстроиться в скученную линию. И наоборот, когда команде противников нужно пройти большее расстояние, защита растягивается, обеспечивая несколько слоев обороны. В некотором смысле это обусловлено тактикой атакующих - при маленьком yardsToGo атаке возможно стоит попытаться проскочить напрямую (такая игра Нырок(dive) когда RB пытается проскочить в коридор между гардом и тэклом).

Следующая страница

Предыдущая страница